{
 "cells": [
  {
   "cell_type": "code",
   "id": "initial_id",
   "metadata": {
    "collapsed": true,
    "ExecuteTime": {
     "end_time": "2025-03-06T08:05:16.515882Z",
     "start_time": "2025-03-06T08:05:10.971550Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "\n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42\n",
    "torch.manual_seed(seed)\n",
    "torch.cuda.manual_seed_all(seed)"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.3.1+cu121\n",
      "cuda:0\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 数据准备",
   "id": "a8681d3d64241051"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-06T08:09:16.238577Z",
     "start_time": "2025-03-06T08:09:16.234521Z"
    }
   },
   "cell_type": "code",
   "source": [
    "with open(\"./shakespeare.txt\", \"r\", encoding=\"utf8\") as file:\n",
    "    text = file.read()"
   ],
   "id": "bb2ea56b56c1455b",
   "outputs": [],
   "execution_count": 3
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "## 构建字典",
   "id": "38c97a8706bb99f4"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-06T08:10:27.040579Z",
     "start_time": "2025-03-06T08:10:26.967671Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 1. 建立字典\n",
    "\n",
    "#去重，留下独立字符，并排序\n",
    "vocab = sorted(set(text))\n",
    "print(f'字典大小：{len(vocab)}')\n",
    "print(f'字典：\\n{vocab}')\n",
    "\n",
    "# 2. 建立id_to_vocab 和 vocab_to_id\n",
    "\n",
    "#每个字符都编好号，enumerate对每一个位置编号，生成的是列表中是元组\n",
    "char2idx = {char: idx for idx, char in enumerate(vocab)}\n",
    "print(f'char2idx：\\n{char2idx}')\n",
    "\n",
    "# 把vocab从列表变为ndarray\n",
    "idx2char = np.array(vocab)\n",
    "\n",
    "# 3. 将数据转换为id序列\n",
    "\n",
    "#把字符都转换为id\n",
    "text_as_int = np.array([char2idx[c] for c in text])\n",
    "print(f'文本长度：{text_as_int.shape}')"
   ],
   "id": "b76d8685a49950d3",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "字典大小：65\n",
      "字典：\n",
      "['\\n', ' ', '!', '$', '&', \"'\", ',', '-', '.', '3', ':', ';', '?', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']\n",
      "char2idx：\n",
      "{'\\n': 0, ' ': 1, '!': 2, '$': 3, '&': 4, \"'\": 5, ',': 6, '-': 7, '.': 8, '3': 9, ':': 10, ';': 11, '?': 12, 'A': 13, 'B': 14, 'C': 15, 'D': 16, 'E': 17, 'F': 18, 'G': 19, 'H': 20, 'I': 21, 'J': 22, 'K': 23, 'L': 24, 'M': 25, 'N': 26, 'O': 27, 'P': 28, 'Q': 29, 'R': 30, 'S': 31, 'T': 32, 'U': 33, 'V': 34, 'W': 35, 'X': 36, 'Y': 37, 'Z': 38, 'a': 39, 'b': 40, 'c': 41, 'd': 42, 'e': 43, 'f': 44, 'g': 45, 'h': 46, 'i': 47, 'j': 48, 'k': 49, 'l': 50, 'm': 51, 'n': 52, 'o': 53, 'p': 54, 'q': 55, 'r': 56, 's': 57, 't': 58, 'u': 59, 'v': 60, 'w': 61, 'x': 62, 'y': 63, 'z': 64}\n",
      "文本长度：(1115394,)\n"
     ]
    }
   ],
   "execution_count": 4
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "## 把莎士比亚文集分成一个一个的样本",
   "id": "91855fbfe4b2608"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-06T08:10:52.661418Z",
     "start_time": "2025-03-06T08:10:52.655955Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from torch.utils.data import Dataset, DataLoader\n",
    "\n",
    "\n",
    "class CharDataset(Dataset):\n",
    "\n",
    "    def __init__(self, text_as_int, seq_length):\n",
    "        self.text_as_int = text_as_int\n",
    "        self.sub_len = seq_length + 1  #一个样本的长度\n",
    "        self.num_seq = len(text_as_int) // self.sub_len  #样本的个数\n",
    "\n",
    "    def __getitem__(self, index):\n",
    "        # index是样本的索引，返回的是一个样本，比如第一个，就是0-100的字符,总计101个字符\n",
    "        return self.text_as_int[index * self.sub_len: (index + 1) * self.sub_len]\n",
    "\n",
    "    def __len__(self):\n",
    "        # 返回样本的个数\n",
    "        return self.num_seq\n",
    "\n",
    "\n",
    "# 定义一个函数，把一个batch的样本转换为输入和输出，输入是前100个字符，输出是后100个字符\n",
    "def collat_fct(batch):\n",
    "    src_list = []  #输入\n",
    "    trg_list = []  #输出\n",
    "    for part in batch:\n",
    "        src_list.append(part[:-1])  #输入\n",
    "        trg_list.append(part[1:])  #输出\n",
    "\n",
    "    src_list = np.array(src_list)  #把列表转换为ndarray\n",
    "    trg_list = np.array(trg_list)  #把列表转换为ndarray\n",
    "    return torch.Tensor(src_list).to(dtype=torch.int64), torch.Tensor(trg_list).to(\n",
    "        dtype=torch.int64)  #返回的是一个元组，元组中的每一个元素是一个torch.Tensor\n",
    "\n",
    "\n",
    "#每个样本的长度是101，也就是100个字符+1个结束符\n",
    "train_ds = CharDataset(text_as_int, 100)\n",
    "train_dl = DataLoader(train_ds, batch_size=64, shuffle=True, collate_fn=collat_fct)"
   ],
   "id": "980cbcf95f6d2aa2",
   "outputs": [],
   "execution_count": 5
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 模型定义",
   "id": "d12540688badc1d1"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-06T08:29:19.395357Z",
     "start_time": "2025-03-06T08:29:19.278642Z"
    }
   },
   "cell_type": "code",
   "source": [
    "class CharLSTM(nn.Module):\n",
    "    def __init__(self, vocab_size, embedding_dim=256, hidden_dim=1024):\n",
    "        super(CharLSTM, self).__init__()\n",
    "        self.embedding = nn.Embedding(vocab_size, embedding_dim)\n",
    "        self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)\n",
    "        self.fc = nn.Linear(hidden_dim, vocab_size)\n",
    "\n",
    "    def forward(self, x, hidden=None):\n",
    "        x = self.embedding(x)\n",
    "        output, hidden = self.lstm(x, hidden)\n",
    "        x = self.fc(output)\n",
    "        return x, hidden\n",
    "\n",
    "\n",
    "vocab_size = len(vocab)\n",
    "sample_inputs = torch.randint(0, vocab_size, (2, 100))\n",
    "\n",
    "print(\"{:=^80}\".format(\" 一层单向 LSTM \"))\n",
    "for key, value in CharLSTM(vocab_size).named_parameters():\n",
    "    print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")\n",
    "\n",
    "CharLSTM(vocab_size)(sample_inputs)[0].shape"
   ],
   "id": "6c40a51c2cd0bd72",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================== 一层单向 LSTM ===================================\n",
      "            embedding.weight            paramerters num: 16640\n",
      "           lstm.weight_ih_l0            paramerters num: 1048576\n",
      "           lstm.weight_hh_l0            paramerters num: 4194304\n",
      "            lstm.bias_ih_l0             paramerters num: 4096\n",
      "            lstm.bias_hh_l0             paramerters num: 4096\n",
      "               fc.weight                paramerters num: 66560\n",
      "                fc.bias                 paramerters num: 65\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "torch.Size([2, 100, 65])"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 6
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 模型训练",
   "id": "d890e7eecb423095"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-06T08:29:39.293672Z",
     "start_time": "2025-03-06T08:29:39.288379Z"
    }
   },
   "cell_type": "code",
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch. \n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = -1\n",
    "\n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "\n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "\n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "\n"
   ],
   "id": "457c62aab181730",
   "outputs": [],
   "execution_count": 7
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-06T08:30:01.724114Z",
     "start_time": "2025-03-06T08:30:00.620753Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 训练\n",
    "def training(\n",
    "        model,\n",
    "        train_loader,\n",
    "        epoch,\n",
    "        loss_fct,\n",
    "        optimizer,\n",
    "        save_ckpt_callback=None,\n",
    "        stateful=False  # 想用stateful，batch里的数据就必须连续，不能打乱\n",
    "):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "    }\n",
    "\n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    hidden = None\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits, hidden = model(datas, hidden=hidden if stateful else None)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits.reshape(-1, vocab_size), labels.reshape(-1))\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "\n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "\n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"step\": global_step\n",
    "                })\n",
    "\n",
    "                # 保存模型权重 save model checkpoint\n",
    "                if save_ckpt_callback is not None:\n",
    "                    save_ckpt_callback(global_step, model.state_dict(), metric=-loss)\n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "\n",
    "    return record_dict\n",
    "\n",
    "\n",
    "epoch = 100\n",
    "\n",
    "model = CharLSTM(vocab_size=vocab_size)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失 \n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "# 2. 定义优化器 采用 adam\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=0.001)\n",
    "\n",
    "# save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(\"checkpoints/text_generation_lstm\", save_step=1000, save_best_only=True)"
   ],
   "id": "c41e852ce5402d92",
   "outputs": [],
   "execution_count": 8
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-06T08:37:43.130073Z",
     "start_time": "2025-03-06T08:30:03.900173Z"
    }
   },
   "cell_type": "code",
   "source": [
    "model = model.to(device)\n",
    "record = training(\n",
    "    model,\n",
    "    train_dl,\n",
    "    epoch,\n",
    "    loss_fct,\n",
    "    optimizer,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    ")"
   ],
   "id": "26d18988c76a0726",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/17300 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "9f6b2ec2172a4d628acd3c1cfb924395"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 9
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-06T08:37:43.484896Z",
     "start_time": "2025-03-06T08:37:43.148152Z"
    }
   },
   "cell_type": "code",
   "source": [
    "plt.plot([i[\"step\"] for i in record[\"train\"][::50]], [i[\"loss\"] for i in record[\"train\"][::50]], label=\"train\")\n",
    "plt.grid()\n",
    "plt.show()"
   ],
   "id": "345e1d39a6292e6b",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ],
      "image/png": "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j3/02v/1M+p+r7Ur+nfgaz766CNT46wdW+26H/hkc49WdWtqzjmyQ+9HRERISfXwww/Ljz/+KIsWLZKoqKh819UDuNqzZ49Z6ufOrTys5/JbR8/E7BgCtNakcePG5jPqtmvzh9Ys5PU7L01lcODAAfn5559l1KhRPr0fWNuc39+6Lo8dO+b1vFZt6yiPwtg37PSdYgUU3T/mz5/vVYuS1/6hZfHbb7+VqnKwaLOwHg88939f2Rd++eUXU4t6se+I4t4PfDKkaCrs0KGDLFiwwKuZRO937dpVSho9I9KAMmPGDFm4cOEF1XC52bBhg1nqmbTSz71582avP1DrS6x58+budTzLzFrHrmWmwwa1hkA/o/6+g4KCvLZf/0C1z4q1/aWpDD7++GNTba1DiX15P9C/Bf1S9NxebZbS/gWev3cNr9pvyaJ/R/qdYIU4XUeHdupB3vMza9OiVm2XhHKxAor229IAq/0NLkb3D+2PYTWBlIZy8HTo0CHTJ8Vz//eFfcGqadXvxTZt2oit9wOHDw9B1h7+U6dONT2677vvPjME2XNUQ0nx4IMPmiGWixcv9hoylpqaap7fs2ePGU6mw27379/v+P777x3169d3dO/e/YKhp3379jXDmHU4abVq1XIdevr444+bkTGTJk2y1fDbv//976YM9DPq8DcdcqnDZ3W0kzUEWYdmL1y40JRF165dza00lYE1Uk0/p/a291Ra94Pk5GQzXFpv+pX22muvmZ+tUSs6BFn/tvXzbtq0yYxmyG0Icrt27RwrV650LF261NGoUSOvYac6IkiHXN55551myKV+f2gZ5BxyGRgY6HjllVdMuehos6IcgpxfOZw/f94MvY6KijK/V8/vCWuExrJly8yIDn1eh6N+/vnn5nc/cuTIElMO+ZWBPvePf/zDjObT/f/nn392tG/f3vyuz507V2r2heSL/D1YQ4h1m3XkXk522w98NqQond9Bv8x1vhQdkqzj4ksi3RFzu+ncKSo2NtYciCpXrmyCmY771wOM5/wY6rfffnMMGDDAjHfXg7se9NPT073W0fk22rZta8pMD3DW/2EHOgy2Zs2aZtsiIyPNfT0wW/Sg9Je//MUMndM/qJtuusl8SZemMlBz5841v/+dO3d6PV5a9wPdltz2fx1uag1D/te//mW+VPVz9+rV64KyOXnypDkQlS9f3gy3vvvuu82XvSedY+W6664z76H7l4afnL755htH48aNTbnoMO2ffvrJYYdy0INyXt8T1hw6a9euNUNE9YQnJCTE0axZM8fzzz/vdQC3eznkVwZ60qbhWw+4erDUYbY6Z1DOE9OSvi8susjfg9IwoX/fGjZystt+4Kf/XF7dCwAAwNXnk31SAACA/RFSAACALRFSAACALRFSAACALRFSAACALRFSAACALRFSAACALRFSAACALRFSAACALRFSAACALRFSAACALRFSAACA2NH/A/rQOrlJYYXPAAAAAElFTkSuQmCC"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 10
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 推理",
   "id": "923bd702e853a61b"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-06T08:37:48.708007Z",
     "start_time": "2025-03-06T08:37:43.489010Z"
    }
   },
   "cell_type": "code",
   "source": [
    "def generate_text(model, start_string, max_len=1000, temperature=1.0, stream=True):\n",
    "    input_eval = torch.Tensor([char2idx[char] for char in start_string]).to(dtype=torch.int64, device=device).reshape(1,\n",
    "                                                                                                                      -1)\n",
    "    hidden = None\n",
    "    text_generated = []\n",
    "    model.eval()\n",
    "    pbar = tqdm(range(max_len))\n",
    "    print(start_string, end=\"\")\n",
    "    with torch.no_grad():\n",
    "        for i in pbar:\n",
    "            logits, hidden = model(input_eval, hidden=hidden)\n",
    "            # 温度采样\n",
    "            logits = logits[0, -1, :] / temperature\n",
    "            # using multinomial to sampling\n",
    "            probs = F.softmax(logits, dim=-1)\n",
    "            idx = torch.multinomial(probs, 1).item()\n",
    "            input_eval = torch.Tensor([idx]).to(dtype=torch.int64, device=device).reshape(1, -1)\n",
    "            text_generated.append(idx)\n",
    "            if stream:\n",
    "                print(idx2char[idx], end=\"\", flush=True)\n",
    "    return \"\".join([idx2char[i] for i in text_generated])\n",
    "\n",
    "\n",
    "torch.manual_seed(seed)\n",
    "torch.cuda.manual_seed_all(seed)\n",
    "# load checkpoints\n",
    "model.load_state_dict(torch.load(\"checkpoints/text_generation_lstm/best.ckpt\", map_location=\"cpu\"))\n",
    "start_string = \"All: \"\n",
    "res = generate_text(model, start_string, max_len=1000, temperature=0.5, stream=True)"
   ],
   "id": "fb1282a83aa819cb",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/1000 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "99dc5d48f61644cbbf849f33de6c683b"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "All: God give you this one thing tongue\n",
      "And charge you send for something that which blows\n",
      "Do please us to their advancement, I cannot\n",
      "Be ybalt-horse for your tribune.\n",
      "\n",
      "SICINIUS:\n",
      "Pray you, go to him.\n",
      "\n",
      "MENENIUS:\n",
      "What is here?\n",
      "\n",
      "ISABELLA:\n",
      "Ay, just; perpetual ducy and defence,\n",
      "That you shall peace with Mowbray's sepul:\n",
      "Because we should infore your conference with his care\n",
      "And that will put you to your forward.\n",
      "\n",
      "KING HENRY VI:\n",
      "From Scotland am I stolen our awford arms?\n",
      "In raised in breathing statues and marriage\n",
      "That Richmond is for Claudio's parch!\n",
      "I do remember well I sport a constant.\n",
      "\n",
      "AUTOLYCUS:\n",
      "I must believe my master; he hath some offence\n",
      "That warm no heir of his affections: therefore,\n",
      "Upon them, good sir; sometime leaving fear;\n",
      "But rather wishing a brace of breath, and stay\n",
      "The country which he spakeness put on thee,\n",
      "O, let them have my princely face.\n",
      "But, greet my father; and I think her answer.\n",
      "\n",
      "KING EDWARD IV:\n",
      "Now am I stand for and revenge this place;\n",
      "The open all this nights did vi"
     ]
    }
   ],
   "execution_count": 11
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": "",
   "id": "42a87b0898747010"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
